package acme.routing.server;


import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

/**
 */
public class RoutingEngineFtpServer
{
  public static final int DATA_INPUT_PORT = 8900;
  public static final int DATA_OUTPUT_PORT = 8901;
  public static final int FTP_SERVER_PORT = 8902;
  public static final int DATA_CHANNEL_PORT = 8903;
  public static final String DATA_OUTPUT_GROUP = "234.2.3.4";
  public static final int DATA_BUFFER_SIZE = 256;
  
  public static final int STATUS_IN_PROGRESS = 0x100;
  public static final int STATUS_COMPLETE = 0x200;
  public static final int STATUS_BUSY = 0x300;
  public static final int STATUS_ERROR = 0x400;
  
  
  public static final int DATA_CONNECTION_TIMEOUT = 60 * 1000; // one minute
  
  /**
   * Sequence number for current or most recent file processed.
   */
  private int currentSequenceNumber = -1;
  
  /**
   * Name of the file currently being processed.  If this is non-null,
   * it must be the file corresponding to currentSequenceNumber.
   */
  private String routeFileInProgress = null;
  
  /**
   * Constructs a default server.
   */
  public RoutingEngineFtpServer()
  {
    
  }

  public void start()
  {
    new Thread(new DatagramListener()).start();
    runServer(FTP_SERVER_PORT);
  }

  /**
   * Basic server loop. Note this version has the following
   * potential deficiencies:
   * <ul> 
   *   <li> if an I/O error occurs, the server
   * will exit rather than attempting to re-create the 
   * server socket.
   *   <li> the server is single-threaded, that is, while
   * handling a connection, new connections cannot be 
   * accepted.
   * </ul>
   * @param port
   *   the port number on which to listen
   */
  private void runServer(int port)
  {
    ServerSocket ss = null;
    try
    {
      ss = new ServerSocket(port);
      while (true)
      {
        System.out.println("Server listening on " + port);
        
        // blocks here until a client attempts to connect
        final Socket s = ss.accept();
        Runnable r = new Runnable()
        {
          public void run()
          {
            handleConnection(s);
          }
        };
        new Thread(r).start();
        
      }      
    }
    catch (IOException e)
    {
      System.out.println("I/O error: " + e);
    }
    finally
    {
      if (ss != null)
      {
        try
        {
          ss.close();
        }
        catch (IOException ignore) {}
      }
    }
  }
  
  
  /**
   * Helper method for handling a client connection.  
   * Closes the socket (and therefore 
   * the associated streams) when the method returns.
   * @param s
   *   Socket representing the client connection
   * @throws IOException
   */
  private void handleConnection(Socket s)
  {
    try
    {
      BufferedReader reader = new BufferedReader(
          new InputStreamReader(s.getInputStream()));
      OutputStream out = s.getOutputStream();
      PrintWriter pw = new PrintWriter(out);

      String request; // = reader.readLine();
      {
        pw.println("220 Ready");
        pw.flush();
        System.out.println("> 220 Ready");
      }
      
      
      boolean done = false;
      while (!done)
      {

        // first line is request
        request = reader.readLine();   
        if (request == null)
        {
          break;  // eof
        }
        System.out.println("< " + request);
        Scanner scanner = new Scanner(request);
        String command = scanner.next().toLowerCase();
        String arg  = null;
        if (scanner.hasNext())
        {
          arg = scanner.next();
        }
        
        if (command.equals("user"))
        {
          pw.println("230 access granted");
          pw.flush();
          System.out.println("> 230 access granted");
        }
        else if (command.startsWith("sys"))
        {
          // meaningless string
          pw.println("215 UNIX Type: L8");
          pw.flush();
          System.out.println("> 215 UNIX Type: L8");
        }
        else if (command.equals("feat"))
        {
          pw.println("211-Extensions supported");
          pw.println("211 End");
          pw.flush();
          System.out.println("> 211-Extensions supported");
          System.out.println("> 211 End");
        }
        else if (command.equals("pasv"))
        {
          //227 =h1,h2,h3,h4,p1,p2
          int port = DATA_CHANNEL_PORT;
          int phi = port / 256;
          int plo = port % 256;
          pw.println("227 Entering Passive Mode (127,0,0,1," + phi + "," + plo + ")");
          pw.flush();
          System.out.println("> 227 Entering Passive Mode (127,0,0,1," + phi + "," + plo + ")");
        }
        else if (command.equals("pwd"))
        {
          pw.println("257 /");
          pw.flush();
          System.out.println("> 257 /");
        }
        else if (command.equals("type"))
        {
          // always assume binary, ignore parameter
          pw.println("250 Ok");
          pw.flush();
          System.out.println("> 250 Ok");
        }
        else if (command.equals("rest"))
        {
          int val = Integer.parseInt(arg);
          if (val == 0)
          {
            pw.println("350 Restarting at 0");
            pw.flush();
            System.out.println("> 350 Restarting at 0");
          }
          else
          {
            pw.println("500 not handling request: " + request);
            pw.flush();
            System.out.println("> 500 not handling request: " + request);
          }
        }
        else if (command.equals("list"))
        {
          pw.println("150 opening data connection");
          pw.flush();
          System.out.println("> 150 opening data connection");
          int ret = startDataChannelTransfer(2223, null, false);
          pw.println(ret + " closing data connection");
          pw.flush();
          System.out.println("> " + ret + " closing data connection");
        }
        else if (command.equals("retr"))
        {
          File f = new File(arg);
          if (!f.exists())
          {
            pw.println("550 file not found");
            pw.flush();
            System.out.println("> 550 file not found");
          }
          else
          {
            pw.println("150 opening data connection");
            pw.flush();
            int ret = startDataChannelTransfer(DATA_CHANNEL_PORT, arg, false);
            pw.println(ret + " closing data connection");
            pw.flush();
            System.out.println("> " + ret + " closing data connection");
          }
        }
        else if (command.equals("stor"))
        {
          File f = new File(arg);
//          if (f.exists())
//          {
//            pw.println("550 file exists");
//            pw.flush();
//            System.out.println("> 550 file exists");
//          }
//          else
//          {
            pw.println("150 opening data connection");            
            pw.flush();
            System.out.println("> 150 opening data connection");
            int ret = startDataChannelTransfer(DATA_CHANNEL_PORT, arg, true);
            pw.println(ret + " closing data connection");
            pw.flush();
            System.out.println("> " + ret + " closing data connection");
//          }
        }

        else 
        {
          pw.println("500 not handling request: " + request);
          pw.flush();
          System.out.println("> 500 not handling request: " + request); 
        }
      }
    }
    catch (IOException e)
    {
      System.out.println(e);
    }
    finally
    {
      try
      {
        s.close();
      }
      catch (IOException ignore){}
    }
  }
 
  
  private int startDataChannelTransfer(int port, String filename, boolean isStor) throws IOException
  {
    ServerSocket ss = null;
    Socket s = null;
    OutputStream out = null;
    InputStream in = null;
    boolean connected = false;
    try
    {
      ss = new ServerSocket(port);
      ss.setSoTimeout(DATA_CONNECTION_TIMEOUT);
      {       
        System.out.println("Server listening on data channel " + port);
        
        // blocks here until a client attempts to connect
        s = ss.accept();
        System.out.println("Got connection on " + port + " from " + s.getInetAddress());        
        connected = true;
        
        if (isStor)
        {
          System.out.println("Reading file " + filename);
          in = s.getInputStream();
          out = new FileOutputStream(filename);
          int c;
          while ((c = in.read()) != -1)
          {
            out.write(c);
          }
          out.flush();
          out.close();
          out = null;
          in.close();
          in = null;
        }
        else
        {
          
          out = s.getOutputStream();
          if (filename != null)
          {
            System.out.println("Sending file " + filename);
            in = new FileInputStream(filename);
            int c;
            while ((c = in.read()) != -1)
            {
              out.write(c);
            }
            out.flush();
            out.close();
            out = null;
            in.close();
            in = null;
          }
          else
          {
            File[] dirs = new File(".").listFiles();
            PrintWriter pw = new PrintWriter(out);
            System.out.println("Sending dir listing:");
            for (int i = 0; i < dirs.length; ++i)
            {
              if (!dirs[i].isDirectory() && dirs[i].getName().indexOf('.') != 0)
              {
                String listing = createListingString(dirs[i]);
                pw.print(listing + "\r\n");
                System.out.println(listing);
              }
            }
            pw.flush();
            pw.close();
            out = null;
          }
        }
        
        // success
        return 226;
      }      
    }
    catch (IOException e)
    {
      System.out.println("I/O error: " + e);
      //throw e;
      if (connected)
      {
        // connection closed by client
        return 426;
      }
      else
      {
        // connection not established
        return 425;
      }
    }
    finally
    {
      // TODO - be sure file is deleted if there was an error
      if (ss != null)
      {
        try
        {
          ss.close();
        }
        catch (IOException ignore) {}
      }
      if (out != null)
      {
        try
        {
          out.close();
        }
        catch (IOException ignore) {}
      }
      if (in != null)
      {
        try
        {
          in.close();
        }
        catch (IOException ignore) {}
      }
    }
  }
  
  private String createListingString(File f) throws IOException
  {
    StringBuilder sb = new StringBuilder();
    if (f.isDirectory())
    {
      sb.append("dr-xr-xr-x");
    }
    else
    {
      sb.append("-r--r--r--");
    }
    sb.append("   1 user group");
    String size = String.format("%13d", f.length());
    sb.append(size);
    sb.append(" Aug 26 16:31 ");
    sb.append(f.getName());
    return sb.toString();
    
  }
  

  
  /**
   * Determines whether a file or directory is beneath
   * a given base directory.  This involves finding
   * the actual files in the file system to get 
   * their 'canonical' representations as File objects
   * in which the pathnames are absolute and contain 
   * no '.' or '..' elements.   
   * @param f
   *   file or directory to be checked
   * @param base
   *   directory against which to check
   * @return
   *   true if f is below base in the filesystem
   * @throws IOException
   *   if there is an error in getting the canonical
   *   files
   */
  private boolean checkIsBelow(File base, File f) throws IOException
  {
    // convert to "canonical" files to normalize the pathnames
    base = base.getCanonicalFile();    
    File current = f.getCanonicalFile();
    
    // make sure that some parent file of the given one
    // is the base directory
    while (current != null)
    {
      if (current.equals(base))
      {
        return true;
      }
      current = current.getParentFile();
    }
    return false;
  }
  
  private static int decodeSequenceNumber(byte[] buf)
  {
    // big endian
    int result = 0;
    for (int i = 0; i < 4; ++i)
    {
      result = (result << 8) | (((int) buf[i]) & 0xff) ; 
    }
    return result;
  }
  
  private static String decodeFileName(byte[] buf, int length)
  {
    // assume we start at offset 4, chars are ASCII
    // string is null terminated
    StringBuilder sb = new StringBuilder("");
    int index = 4;
    while (index < length && buf[index] != 0)
    {
      char c = (char) (((int) buf[index]) & 0xff);
      sb.append(c);
      ++index;
    }
    return sb.toString();
  }
  
  private void initiateCalc(String filename)
  {
    new Thread(new RouteCalc(filename)).start();
  }
  
  private void broadcast(int sequenceNumber, int response)
  {
    System.out.println("Sending datagram: sequence number " + sequenceNumber + " response " + response);
    try
    {
      InetAddress addr = InetAddress.getByName(DATA_OUTPUT_GROUP);
      DatagramSocket s = new DatagramSocket();
      byte[] buf;// = new byte[256];
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      DataOutputStream dos = new DataOutputStream(baos);
      dos.writeInt(sequenceNumber);
      dos.writeInt(response);
      buf = baos.toByteArray();
      synchronized(this)
      {
        DatagramPacket packet = new DatagramPacket (buf, buf.length, addr, DATA_OUTPUT_PORT);
        s.send(packet);
      }
    }
    catch (IOException e)
    {
      System.out.println("Error sending datagram: " + e);
    }
  }
  
  private class DatagramListener implements Runnable
  {
    public void run()
    {
      while (true)
      {
        DatagramSocket socket = null;
        try
        {
          socket = new DatagramSocket(DATA_INPUT_PORT);
          while (true)
          {
            byte[] buf = new byte[DATA_BUFFER_SIZE];
            DatagramPacket packet = new DatagramPacket(buf, buf.length);
            socket.receive(packet);
            buf = packet.getData();
            int length = packet.getLength();
            if (length < 4)
            {
              // this is an error, ignore
              System.out.println("Error - invalid packet");
            }
            else 
            {
              int seq = decodeSequenceNumber(buf);

              if (length == 4)
              {
                System.out.println("Datagram Listener: status request for " + seq);
                // this is a request for status on a given sequence number
                synchronized(RoutingEngineFtpServer.this)
                {
                  if (seq < currentSequenceNumber)
                  {
                    // prior sequence numbers are always assumed complete
                    broadcast(seq, STATUS_COMPLETE);
                  }
                  else if (seq > currentSequenceNumber)
                  {
                    // future seq number is an error
                    broadcast(seq, STATUS_ERROR);
                  }
                  else
                  {
                    if (routeFileInProgress != null)
                    {
                      broadcast(seq, STATUS_IN_PROGRESS);
                    }
                    else
                    {
                      broadcast(seq, STATUS_COMPLETE);
                    }
                  }
                }
              }
              else
              {
                // this initiates a new request
                if (seq <= currentSequenceNumber)
                {
                  // has to be a new sequence number
                  broadcast(seq, STATUS_ERROR);
                }
                else
                {
                  if (routeFileInProgress != null)
                  {
                    broadcast(seq, STATUS_BUSY);
                  }
                  else
                  {
                    // get the file name and start a new one
                    String s = decodeFileName(buf, length);
                    System.out.println("Datagram Listener: calc request sequence number " + seq + " file " + s);
                    if (s.length() == 0)
                    {
                      broadcast(seq, STATUS_ERROR);
                    }
                    else
                    {
                      routeFileInProgress = s;
                      currentSequenceNumber = seq;
                      broadcast(seq, STATUS_IN_PROGRESS);
                      initiateCalc(routeFileInProgress);
                    }
                  }
                }
              }
            }
          }
        }
        catch (IOException e)
        {
          System.out.println("Error in datagram listener: " + e);
        }
        finally
        {
          if (socket != null)
          {
            socket.close();
          }
        }
      }
    }
  }
  
  private class RouteCalc implements Runnable
  {
    String filename;
    public RouteCalc(String filename)
    {
      this.filename = filename;
    }
    
    public void run()
    {
      System.out.println("Routing Engine: calculation in progress for " + filename);
      
      int result;
      try
      {
        // do calc
        Thread.sleep(10000);
        
        // create new file
        new RoutingEngineConnector().doRouteCalculation(filename);
//        File f = new File(filename + "_result");
//        PrintWriter pw = new PrintWriter(f);
//        pw.println("Test file.");
//        pw.close();
        result = STATUS_COMPLETE;
      }
      catch (Exception e)
      {
        // allow error to be returned
        System.out.println("Routing Engine: Error in route calculation for file " + filename + ": " + e);
        result = STATUS_ERROR;
      }
      
      synchronized(this)
      {
        routeFileInProgress = null;
        broadcast(currentSequenceNumber, result);
      }
    }
  }
}
